package com.weilus.gateway.filters;

import lombok.Data;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.PathContainer;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.util.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPatternParser;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * spring:
 *   cloud:
 *     gateway:
 *       authentication:
 *       - id: flux-call
 *         auth:
 *         - GET /test/**=authenticated()               #需要认证才能访问
 *         - PUT /test/add=auth2.hasScope('user-info')  #认证且指定域权限才能访问
 *         - /test/other=hasRole('aa,bb')               #认证且指定权限才能访问
 */
@Data
@RefreshScope
@Configuration
@ConfigurationProperties(prefix = "spring.cloud.gateway.authentication")
public class AuthProperties extends ArrayList<AuthProperties.RouterLimiter> implements InitializingBean {
    private Map<String,Set<AuthMatcher>> authMatchers = new HashMap<>();
    @Autowired
    private ApplicationContext context;


    public Set<AuthMatcher> getMatchers(String routeId, HttpMethod method, PathContainer path){
        Set<AuthMatcher> matchers = authMatchers.get(routeId);
        if (CollectionUtils.isEmpty(matchers)){
            return Collections.EMPTY_SET;
        }
        return matchers.stream()
                .filter(m -> m.getPathPattern().matches(path) && (m.getMethod() == null || m.getMethod() == method))
                .collect(Collectors.toSet());
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        if(!CollectionUtils.isEmpty(this)){
            this.authMatchers = this.stream().collect(Collectors.toMap(RouterLimiter::getRouteId, t->
                    t.getAuth().stream().map(AuthMatcher::new).collect(Collectors.toSet())
            ));
        }
    }

    @Data
    public static class RouterLimiter{
        private String routeId;
        private List<String> auth;
    }

    @Data
    public static class AuthMatcher{
        public final static Pattern scopePattern = Pattern.compile("^auth2.hasScope\\('(.*?)'\\)$");
        public final static Pattern rolePattern = Pattern.compile("^hasRole\\('(.*?)'\\)$");
        public final static PathPatternParser pathPatternParser = new PathPatternParser();

        private PathPattern pathPattern;
        private HttpMethod method;
        private boolean requiredAuthenticated;
        private Set<String> requiredScopes;
        private Set<GrantedAuthority> requiredAuthorities;

        public AuthMatcher(String config){
            String[] arr = StringUtils.delimitedListToStringArray(config,"=");
            String pathConfig = arr[0],authConfig = arr[1];
            String[] pathArr = StringUtils.delimitedListToStringArray(pathConfig," ");
            this.pathPattern = pathPatternParser.parse(pathArr.length == 1 ? pathArr[0] : pathArr[1]);
            this.method = HttpMethod.resolve(pathArr.length == 1 ? null : pathArr[0]);
            this.requiredAuthenticated = authConfig.equals("authenticated()");
            Matcher scopeMatcher = scopePattern.matcher(authConfig);
            Matcher roleMatcher = rolePattern.matcher(authConfig);
            if(scopeMatcher.find()){
                this.requiredAuthenticated = true;
                this.requiredScopes = StringUtils.commaDelimitedListToSet(scopeMatcher.group(1));
            }else if(roleMatcher.find()){
                this.requiredAuthenticated = true;
                this.requiredAuthorities = StringUtils.commaDelimitedListToSet(scopeMatcher.group(1)).stream()
                        .distinct()
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toSet());
            }
        }

        @Override
        public String toString() {
            return "{" +
                    "method=" + method +
                    ",pathPattern=" + pathPattern +
                    ", requiredAuthenticated=" + requiredAuthenticated +
                    ", requiredScopes=" + requiredScopes +
                    ", requiredAuthorities=" + requiredAuthorities +
                    '}';
        }
    }
}
